Zero Allocations for TryFormatCanonical - #71
Merged
Conversation
MetadataValue.TryFormatCanonical honors its span contract only for Double and Single; the remaining ten kinds detour through ToCanonicalString and allocate a throwaway string. The same missing primitives cost an allocation per candidate on the TryGetXxx read path. Key decisions recorded in the plan: - Port the formatters from dotnet/runtime v6.0.36 rather than writing them from scratch. That line predates the intrinsics rewrites, so it is scalar and compiles on netstandard2.0, and XsdDuration is the normative source of the TimeSpan encoding the validators compare against. - Cover UTF-8 as well as UTF-16 via the existing TCodeUnit renderer, closing the serializer integration deferred by #61. - Ship exactly one implementation for both assets. Whether net10.0 should call the framework span formatters is deferred to a follow-up issue; this plan only produces the benchmark evidence. - Do not route String, Char, or Uri through the UTF-8 API in the serializers: Utf8JsonWriter does not agree bytewise with pre-transcoded replacement bytes under UnsafeRelaxedJsonEscaping. - Normalize DateTimeKind.Local to UTC in the formatter, matching FromDateTime, which keeps the 28-character bound. - Preserve the allocation-free ToCanonicalString paths for Null, Boolean, String, and Uri. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZTXiVaNguCvmjrTjwWLfQ
The netstandard2.0 layout guard called decimal.GetBits(decimal), which allocates a four-element array on first use. That contradicted the unqualified no-allocation criterion, and the warmed-up allocation test could not have caught it: warm-up triggers type initialization before measurement starts. Seed the probe from decimal(int, int, int, bool, byte) instead. The constructor defines the logical representation just as GetBits does, so the check is no weaker, and it touches nothing on the heap. Also isolate the guard in a decimal-specific helper so a layout failure cannot disable integer, date, TimeSpan, and Guid formatting, and store its outcome rather than throwing from the initializer, so callers see PlatformNotSupportedException instead of TypeInitializationException wrapping it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZTXiVaNguCvmjrTjwWLfQ
None of these change the design; they stop the plan from asserting more than is true. - The read-path rationale claimed every text-accepting TryGetXxx reformats to validate. Nine do; TryGetDecimal, TryGetChar, and TryGetUri accept text without reformatting. - "The remaining canonical encodings are ASCII" excluded nothing, but Char is included in that set and may be non-ASCII. Scope it to the non-text encodings and name the exception. - bytesWritten is not guaranteed to equal charsWritten for the text-bearing kinds, rather than never equal to it: ASCII String, Char, and Uri values still have equal counts, so the assertion has to be conditioned on content. - The one-implementation criterion read as banning the decimal extraction and transcoding shims the plan itself specifies. Require one canonical renderer, and confine target-specific code to those two shims, neither of which decides the output text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZTXiVaNguCvmjrTjwWLfQ
Tighten the allocation-free canonical formatting plan from 248 to 191 lines. All 17 acceptance criteria and every normative instruction are preserved; the reduction is redundant prose, upstream line counts, and a Scope section whose content already appeared elsewhere. Restore five justifications that the condensation had removed: - the derivations of MaximumDayNumber and MaximumTimeOfDayTicks, which netstandard2.0 cannot check against a BCL type - why the encodings must be adapted rather than rederived: the TryGetXxx validators pin them, so a wrong rule breaks data already on the wire - the evidence that the decimal layout assumption is real, namely that GetBits documents the logical representation and upstream DecCalc carries an explicit BIGENDIAN layout - Numbers/ as the established provenance pattern to follow - the rule behind the serializer adoption bullets: the writer transcodes UTF-16 itself and receives UTF-8 only for ASCII kinds Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZTXiVaNguCvmjrTjwWLfQ
Add shared UTF-16 and UTF-8 canonical formatters for primitive metadata values, and route validation and JSON serialization through allocation-free span paths. Cover the new contracts with cross-asset tests, allocation assertions, benchmarks, provenance, and plan-deviation documentation. Unify floating-point formatting under the partial CanonicalTextFormatter and expose the low-level CanonicalCodeUnit helper in the focused Text namespace. BREAKING CHANGE: CanonicalFloatingPointFormatter has been removed. Use CanonicalTextFormatter for Double and Single canonical formatting. Closes #70
Handle empty UTF-16 input consistently across package assets and keep UTF-8 capacity checks atomic for every representable span. Assert fixed-buffer invariants, consolidate unreachable failure paths, and extend the shared canonical corpus with empty text and negative scaled decimals.
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
17 tasks
Minimum allowed line rate is |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #70
Plan Deviations for Allocation-Free Canonical Formatting
Referenced Plans
0058-fix-runtime-specific-number-metadata.mdintroduced the publicCanonicalFloatingPointFormatterfor runtime-independentdoubleandfloattext.0061-add-utf-8-floating-point-formatting.mdadded its UTF-8 overloads and established theprivate per-call Dragon4 test seam.
0070-0-try-format-canonical-zero-allocations.mdintroducedCanonicalTextFormatterfor theremaining primitive metadata kinds and explicitly retained the existing floating-point formatter.
Deviations
Unified formatter surface
The implemented API no longer exposes a separate
CanonicalFloatingPointFormatter. Its publicconstants and its
Format,TryFormat, andTryFormatUtf8overloads fordoubleandfloatare nowmembers of the partial
CanonicalTextFormatterinLight.PortableResults.Text. The floating-pointsurface resides in
CanonicalTextFormatter.FloatingPoint.cs; the Grisu3, Dragon4, number-buffer, andcompatibility implementations remain internal types in
Light.PortableResults.Numbers.Production call sites, tests, and benchmarks use the unified formatter. The floating-point tests
continue to locate the two private generic
TryFormatCoreoverloads for their forced-Dragon4 corpus,so the test seam required by plans 0058 and 0061 is unchanged apart from its declaring type.
Rationale
Once plan 0070 added canonical formatting for every other primitive kind, retaining a second public
formatter made the API harder to discover and forced consumers such as
MetadataValueto dispatchbetween two classes with the same destination, atomicity, and allocation contracts. A partial class
keeps the large floating-point implementation in its own source file without creating a runtime or
performance boundary. The library is not yet stable and permits breaking API changes, so consolidating
the surface now is preferable to preserving the historical split through forwarding APIs.
This deviation changes API ownership and source organization only. Floating-point text, exceptions,
capacity behavior, allocation behavior, UTF-8 output, algorithm selection, and wire formats remain
unchanged.
Public code-unit helper
Plan 0070 originally required the shared code-unit helper to remain unexposed. The implemented
CanonicalCodeUnitis instead a public, top-level type in the focusedLight.PortableResults.Textnamespace. This follows the repository's hide-in-plain-sight approach: advanced implementation-oriented
types remain accessible without adding them to the main namespace or nesting them inside a facade.
The type and its
FromAscii<TCodeUnit>method are XML-documented. The public method supportsbyteandchar, matching the formatter's UTF-8 and UTF-16 destinations, and rejects other unmanagedtypes with
NotSupportedException. The supported generic instantiations retain their directallocation-free reinterpretation paths.